package fi.iki.joker.mobilogger;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;

import android.R;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.AssetFileDescriptor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.text.Spannable;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.Spinner;
import android.widget.TextView;
import fi.iki.joker.mobilogger.blogs.BlogConfigConstants;
import fi.iki.joker.mobilogger.blogs.BlogInterface;
import fi.iki.joker.mobilogger.blogs.BlogInterfaceFactory;
import fi.iki.joker.mobilogger.db.BlogConfigBean;
import fi.iki.joker.mobilogger.db.BlogEntryBean;
import fi.iki.joker.mobilogger.db.DBUtil;
import fi.iki.joker.mobilogger.db.ImageRepoBean;
import fi.iki.joker.mobilogger.editor.SpannableBufferHelper;
import fi.iki.joker.mobilogger.img.ImageRepoInterfaceFactory;
import fi.iki.joker.mobilogger.img.ImageRepository;
import fi.iki.joker.mobilogger.img.ImageRepositoryConstants;
import fi.iki.joker.mobilogger.util.AlertUtil;
import fi.iki.joker.mobilogger.util.PicasaWebUtil;

/**
 * The PreviewDialog is the entrypoint to publishing, i.e. each entry to be published is previewed first. 
 * Also, the user is able to change the post's publish configuration from the publish dialog to make it easier to
 * post the same entry to many different blogs (TODO: why would one do this is a good question, so we need to follow up
 * whether it's actually used. If not let's remove that because it otherwise adds complexity without purpose. )
 * 
 * TODO: The thread communication (and progress dialog) implementation is not very clean nor efficient. It should be refactored
 * so that it would just use int codes in messages rather than sending the same information in Strings.  
 * @author juhak
 *
 */

public class PreviewDialog extends Activity implements AdapterView.OnItemSelectedListener,
	View.OnClickListener {

	private final int PUBLISH_STATE_PREPARING_BLOG = 1;
	private final int PUBLISH_STATE_AUTHENTICATING_BLOG = 2;
	private final int PUBLISH_STATE_CONTACTING_SERVER = 3;
	private final int PUBLISH_STATE_CREATING_ENTRY = 4;
	private final int PUBLISH_STATE_BLOG_DONE = 5;
	private final int PUBLISH_STATE_PREPARING_IMAGE = 6;
	private final int PUBLISH_STATE_AUTHENTICATING_IMAGE = 7;
	private final int PUBLISH_STATE_CONTACTING_IMAGE_SERVER = 8;
	private final int PUBLISH_STATE_UPLOADING_IMAGE = 9;
	private final int PUBLISH_STATE_IMAGE_DONE = 10;
	
	/**
	 * Types of threads that are involved in publishing of content
	 */
	
	private final String THREAD_TYPE_IMAGE = "imagePublishThread";
	private final String THREAD_TYPE_ENTRY = "entryPublishThread";
	
	/**
	 * This is a list of threads that are each used to publish one item in the blog entry configured site .
	 * First in the list are the embedded objects like images, then what follows is the text of the blog entry.
	 * The threads are run SEQUENTIALLY, so there is not need to synchronize on the list access or on any of the
	 * thread local references.
	 */
	
	private LinkedList<Thread> publishSequence = null;
	LinkedList<Uri> imageContentToPublish = null;
	private int currentJobId = 0;
	private String blogPublishStatus = "---";
	private String imagesPublishStatus = "---";
	
	/**
	 * Helper for handling the spanned content entries.
	 */
	private SpannableBufferHelper bufferHelper = null;
	
	private BlogEntryBean myEntryBean = null;
	//the handler for the UI callback from the publish thread
	final Handler mHandler = new Handler() {
		@Override
		public void handleMessage(Message msg) {
			Bundle content = msg.getData();
			int progressId = content.getInt(MSG_KEY);
			if(progressId == PUBLISH_STATE_PREPARING_BLOG) {
				blogPublishStatus = "Preparing blog config...";
			} else if(progressId == PUBLISH_STATE_AUTHENTICATING_BLOG) {
				blogPublishStatus = "Authenticating...";
			} else if(progressId == PUBLISH_STATE_CONTACTING_SERVER) {
				blogPublishStatus = "Contacting server...";
			} else if(progressId == PUBLISH_STATE_CREATING_ENTRY) {
				blogPublishStatus = "Creating new entry...";
			} else if(progressId == PUBLISH_STATE_BLOG_DONE) {
				blogPublishStatus = "Done...";
			} else if(progressId == PUBLISH_STATE_PREPARING_IMAGE) {
				imagesPublishStatus = "Preparing...";
			} else if(progressId == PUBLISH_STATE_UPLOADING_IMAGE) {
				imagesPublishStatus = "Uploading...";
			} else if(progressId == PUBLISH_STATE_IMAGE_DONE) {
				imagesPublishStatus = "Done.";
				// content should now have the published image's url in it.
				String currentPublishedImageUrl = content.getString(IMG_URL_KEY);
				String currentUriToReplace = content.getString(IMG_REPLACE_URI_KEY);
				Log.d(TAG,"Got status that image is done. The associated image URL is: "+currentPublishedImageUrl);
				Log.d(TAG,"Replacing the local URI: "+currentUriToReplace);
				if(bufferHelper == null) {
					bufferHelper = new SpannableBufferHelper();
				}
				bufferHelper.replaceLocalImageUriWithURL(myEntryBean.getBlogEntry(), currentUriToReplace, currentPublishedImageUrl);
			}
			publishProgress.setMessage("Entry: "+blogPublishStatus+" Images: "+imagesPublishStatus);
		}
	};
	private final String MSG_KEY="value";
	private final String IMG_URL_KEY="imageUrl";
	private final String IMG_REPLACE_URI_KEY = "replaceThisUriWithImageURL";
	private ProgressDialog publishProgress = null;
	HashMap<Integer, Integer> configItemOrder = null;
	int publishStatus = 0;
	private DBUtil db = null;
	int publishToConfigID = -1;
	private int publishImagesToRepoID = -1;
	private int[] imageRepoIdList = null; 
	
	/**
	 * This runnable is called when the threads involved in publishing (ones stored in the publishSequence) are exiting.
	 * It manages the thread list counter (currentJobId) and performs the finalization actions of the thread list.
	 */
	
	final Runnable mThreadFinished = new Runnable() {
		public void run () {
			if(currentJobId == publishSequence.size()-1) {
				//perform fixing of content in case URLs need changing.
				//continue from here.
				// if there's no longer stuff to publish, reset the thread list and relevant variables and report status
				blogPublishStatus = "---";
				imagesPublishStatus = "---";
				publishSequence = null;
				currentJobId = 0;
				imageContentToPublish = null;
				showFinalPublishedStatus();
			} else if (currentJobId < publishSequence.size()) {
				// if there's more content to publish, start the next thread.
				currentJobId++;
				Log.d(TAG,"Running thread from the thread list with ID: "+currentJobId);
				publishSequence.get(currentJobId).start();
			} else if (currentJobId > publishSequence.size()) {
				// sanity check
				throw new RuntimeException("The publish sequence's current job ID is larger than the size of the Thread List! Escaping!.");
			}
		}
	};
	//Log.d(TAG,"Set position ["+curPosition+"]->"+order[position]);
	private final String TAG = "PreviewDialog";
	
	@Override
	protected void onCreate(Bundle icicle) {
		// TODO Auto-generated method stub
		super.onCreate(icicle);
		//NamespaceContextImpl nsi = new NamespaceContextImpl();
		setContentView(fi.iki.joker.mobilogger.R.layout.previewdialog);
		Spinner blogconfigs = (Spinner) findViewById(fi.iki.joker.mobilogger.R.id.blognamespinner);
		Button publishButton = (Button) findViewById(fi.iki.joker.mobilogger.R.id.publish_button);
		if(db == null) {
			db = new DBUtil();
		}
		LinkedList<BlogConfigBean> items = db.getBlogNames(this);
		//put the config item id numbers so that they can be looked up from the spinner indices
		//but the publishing. This is because if user changes the selection, we need to change
		//the config
		if(configItemOrder == null) {
			configItemOrder = new HashMap<Integer,Integer>(items.size());
		}
		for(int i=0; i<items.size();i++) {
			try {
				configItemOrder.put(new Integer(i), new Integer(items.get(i).getId()));
			} catch (NullPointerException ne) {
				Log.d(TAG,"Config items contains a null entry at "+i+"! Default to first config.");
				configItemOrder.put(new Integer(i), new Integer(0));
			}
		}
		Object res = new ArrayAdapter(this,android.R.layout.simple_spinner_item,items);
		ArrayAdapter<CharSequence> adapter = (ArrayAdapter<CharSequence>)res;
		adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
		blogconfigs.setAdapter(adapter);
		blogconfigs.setOnItemSelectedListener(this);
		publishButton.setOnClickListener(this);
		TextView content = (TextView) findViewById(fi.iki.joker.mobilogger.R.id.entrypreview);
		
		//get the selected values from the caller:
		Intent i = this.getIntent();
		if(i == null) {
			AlertUtil.showAlert(this,"Intent data missing!",
					"The intent used to launch this Activity was null!");
			this.finish();
		} else {
			Uri uri = i.getData();
			int entryid = -1;
			try {
				entryid = Integer.parseInt(uri.getLastPathSegment());
			} catch (NumberFormatException e) {
				AlertUtil.showAlert(this,"Intent data is invalid!",
						"The intent data used to launch this Activity cannot be parsed!");
				this.finish();
			}
			this.publishToConfigID = i.getIntExtra("fi.iki.joker.mobilogger.PublishConfigID", -1);
			if(this.publishToConfigID == -1) {
				AlertUtil.showAlert(this,"Intent data is invalid!",
						"The intent data (extra int) used to launch this Activity cannot be found!");
				this.finish();
			}
			//config id and blog entry id are now ok...
			this.myEntryBean = db.getBlogEntryById(this.getApplication(),entryid);
			if(this.myEntryBean == null) {
				AlertUtil.showAlert(this,"Intent data is invalid!", 
						"The intent data cannot be used to find the blog entry!");
			}
			//the "items" list has the id and name properties of 
			//blog configs. set the spinner's selected index to
			//be that index of the list which has "publishToConfigID"
			//in it's ID property.
			int selectedConfig = findListIndexOfConfig(items,this.publishToConfigID);
			if(selectedConfig != -1) {
				blogconfigs.setSelection(selectedConfig);
				this.myEntryBean.setPublishedIn(this.publishToConfigID);
			} else {
				AlertUtil.showAlert(this,"Intent data is invalid!",
						"The intent data (config ID) used to launch this Activity cannot be resolved to a config in database!");
				this.finish();
			}
			// if this blog entry contains media that is only locally stored to the device, these need to be published too.
			// The following code checks if that is the case with images, and if it is, adds a spinner to the layout, which
			// allows the user to select the image hosting place where the images will be kept.
			// If there's no such place configured. This is told to the user.
			if(bufferHelper == null) {
				bufferHelper = new SpannableBufferHelper();
			}
			
			if(bufferHelper.hasLocallyStoredImages(myEntryBean.getBlogEntry(), this)) {
				if(db == null) {
					db = new DBUtil();
				}
				ImageRepoBean[] repositories = db.getImageRepositories(this);
				if(repositories == null || repositories.length == 0) {
					AlertUtil.showAlert(this, "Warning - local images", "Please be advised that this posting contains references to images that" +
							" are available on this device only. These need to be published somewhere (an online image repository) and you have not " +
							"configured any. If you continue with publishing, some images will not display correctly on the posting.");
				} else {
					//populate the spinner to allow user to select where to publish the images
					Spinner imageRepoSelectSpinner = (Spinner)findViewById(fi.iki.joker.mobilogger.R.id.preview_imageselectspinner);
					LinkedList<String> repoNames = new LinkedList<String>();
					imageRepoIdList = new int[repositories.length];
					for(int j = 0; j < repositories.length; j++) {
						repoNames.add(repositories[j].getTitle());
						imageRepoIdList[j] = repositories[j].getId(); 
					}
					Object imgRepoAdapter = new ArrayAdapter(this,android.R.layout.simple_spinner_item,repoNames);
					ArrayAdapter<CharSequence> seqAdapter = (ArrayAdapter<CharSequence>)imgRepoAdapter;
					seqAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
					imageRepoSelectSpinner.setAdapter(seqAdapter);
					imageRepoSelectSpinner.setOnItemSelectedListener(this);
					publishImagesToRepoID = imageRepoIdList[imageRepoSelectSpinner.getSelectedItemPosition()];
				}
			} else {
				// No locally stored images, don't show the image repository spinner.
				TextView imageRepoSelectTitle = (TextView)findViewById(fi.iki.joker.mobilogger.R.id.preview_imageselectspinnertitle);
				Spinner imageRepoSelectSpinner = (Spinner)findViewById(fi.iki.joker.mobilogger.R.id.preview_imageselectspinner);
				imageRepoSelectTitle.setVisibility(View.GONE);
				imageRepoSelectSpinner.setVisibility(View.GONE);
			}
		}
		
		content.append(myEntryBean.getTitle());
		content.append("\n");
		content.append(myEntryBean.getBlogEntry());	
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		// TODO Auto-generated method stub
		return super.onCreateOptionsMenu(menu);
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		// TODO Auto-generated method stub
		return super.onOptionsItemSelected(item);
	}
    
	public void onItemSelected(AdapterView parent, View v, int position, long id) {
		if(v.getId() == fi.iki.joker.mobilogger.R.id.blognamespinner) {
			this.myEntryBean.setPublishedIn(position);
			publishToConfigID = configItemOrder.get(position);
		} else if(v.getId() == fi.iki.joker.mobilogger.R.id.preview_imageselectspinner) {
			publishImagesToRepoID = imageRepoIdList[position];
		}
    }

    public void onNothingSelected(AdapterView parent) {
    }

	public void onClick(View v) {
		this.publishBlogEntry();
	}
	
	/**
	 * Each embedded object of the blog posting is published in it's own thread and finally the blog text is published in it's own.
	 * The threads are run in sequence, i.e. there is no parallelism involved, thus no need to synchronize anywhere. 
	 */
	
	
	private void publishBlogEntry() {
		final Activity thread_parent = this;
		publishProgress = ProgressDialog.show(this, "Publishing blog entry", "Starting to publish blog entry...");
		SpannableBufferHelper bufferHelper = new SpannableBufferHelper();
		imageContentToPublish = bufferHelper.getLocalImageUris(myEntryBean.getBlogEntry(),thread_parent);	
		if(db == null) {
			db = new DBUtil();
		}
		// only create the image repo bean instance if there's image uris in the posting that need
		// to be published to some repository.
		ImageRepoBean irb = null;
		if(!imageContentToPublish.isEmpty()) {
			irb = db.getImageRepository(thread_parent, publishImagesToRepoID);
			if(irb == null) {
				Log.e(TAG, "Database error while publishing image content."+
					" Failed to get repostory corresponding to ID:"+publishImagesToRepoID);
				AlertUtil.showAlert(thread_parent, "Error in image repository configuration", 
					"The selected image repository has bad or missing configuration in the application database. Can't publish images.");
				publishProgress.dismiss();
				return;
			}
		}
		publishSequence = new LinkedList<Thread>();

		final Thread publish = new Thread (THREAD_TYPE_ENTRY) {
			public void run() {
				Bundle status = new Bundle();
				mHandler.getLooper().prepare();
				Message statusMsg = mHandler.obtainMessage();
				publishStatus = 0;
				status.putInt(MSG_KEY, PUBLISH_STATE_PREPARING_BLOG);
				statusMsg.setData(status);
				mHandler.sendMessage(statusMsg);
				boolean publishOk = false;
				CheckBox draftCb = (CheckBox) findViewById(fi.iki.joker.mobilogger.R.id.blog_options_asdraftcheckbox);
				if(draftCb != null) {
					myEntryBean.setDraft(draftCb.isChecked());
				}
				//here we should read the blog config number, if it has been changed, from the preview
				//activity and change publish API and other details accordingly
				int confignum = publishToConfigID;
				if(db == null) {
					db = new DBUtil();
				}
				BlogConfigBean bc = null;
				bc = db.getBlogConfigById(thread_parent,confignum);
				BlogConfigConstants.BlogInterfaceType typeEnum = BlogConfigConstants.getInterfaceTypeByNumber(bc.getPostmethod());
				BlogInterface blogapi = null;
				blogapi = BlogInterfaceFactory.getInstance(typeEnum);
				Log.d(TAG,"Using interface type: "+typeEnum);
				Log.d(TAG,"Preparing the API with saved editor data: "+bc.getPostConfig());
				blogapi.setInstanceConfig(bc.getPostConfig());
				Log.d(TAG,"Trying to get auth token for config id: "+confignum+" with username "+bc.getUsername());
				status.putInt(MSG_KEY, PUBLISH_STATE_AUTHENTICATING_BLOG);
				statusMsg = mHandler.obtainMessage();
				statusMsg.setData(status);
				mHandler.sendMessage(statusMsg);
				String auth_id = blogapi.getAuthId(bc.getUsername(), bc.getPassword());
				publishStatus = 1;
				Log.d(TAG,"Got auth token:"+auth_id);
				publishStatus = 2;
				if(auth_id != null) {
				status.putInt(MSG_KEY, PUBLISH_STATE_CONTACTING_SERVER);
				statusMsg = mHandler.obtainMessage();
				statusMsg.setData(status);
				mHandler.sendMessage(statusMsg);
				String postUri = blogapi.getPostUrl(auth_id);
				SpannableBufferHelper helper = new SpannableBufferHelper();
				status.putInt(MSG_KEY, PUBLISH_STATE_CREATING_ENTRY);
				statusMsg = mHandler.obtainMessage();
				statusMsg.setData(status);
				mHandler.sendMessage(statusMsg);
				publishOk = blogapi.createPost(thread_parent, auth_id, postUri, null, 
						myEntryBean.getTitle(), null, 
						helper.SpannableToXHTML((Spannable)(myEntryBean.getBlogEntry())), 
						bc.getUsername(), bc.getUsername(), myEntryBean.isDraft());
				} else {
					publishStatus = 3;	
				}
				status.putInt(MSG_KEY, PUBLISH_STATE_BLOG_DONE);
				statusMsg = mHandler.obtainMessage();
				statusMsg.setData(status);
				mHandler.sendMessage(statusMsg);
				if(publishOk) {
					Log.d(TAG,"Post published successfully!");
					publishStatus = 5;
				} else {
					Log.d(TAG,"Publishing of the post failed!");
					publishStatus = 4;
				}
				mHandler.post(mThreadFinished);
			}
		};
		if(imageContentToPublish == null || imageContentToPublish.size() == 0) {
			Log.d(TAG,"There are no locally stored images that need to be published. Not creating any Threads for image publishing.");
		} else {
			//create as many image publish threads as there are local images to publish
			int imagesToPublish = imageContentToPublish.size();
			final ImageRepoBean irbFinal = irb;
			for(int i = 0; i < imagesToPublish; i++) {
				final Uri uriToPublish = imageContentToPublish.get(i);
				publishSequence.add(
						new Thread (THREAD_TYPE_IMAGE) {
							public void run() {
								// Only execute if the current thread is actuall a image publishing job. This condition is only a sanity check.
								Bundle status = new Bundle();
								mHandler.getLooper().prepare();
								Message statusMsg = mHandler.obtainMessage();
								status.putInt(MSG_KEY, PUBLISH_STATE_PREPARING_IMAGE);
								statusMsg.setData(status);
								mHandler.sendMessage(statusMsg);
								ImageRepository ir = ImageRepoInterfaceFactory.getInstance(ImageRepositoryConstants.getInterfaceTypeEnum(irbFinal.getInterfaceType()));
								String publishedUrl = ir.publishImage(thread_parent, irbFinal, uriToPublish, uriToPublish.toString()+".jpg");
								statusMsg = mHandler.obtainMessage();
								status.putInt(MSG_KEY, PUBLISH_STATE_IMAGE_DONE);
								//send the published image's URL to the message handle so that the blog entry content
								//can be fixed accordingly, also appends the local device URI for the content so that
								//we can distinguish which image span to change.
								status.putString(IMG_URL_KEY, publishedUrl);
								status.putString(IMG_REPLACE_URI_KEY,uriToPublish.toString());
								statusMsg.setData(status);
								mHandler.sendMessage(statusMsg);
								mHandler.post(mThreadFinished);
							}
						}
				);
			}
		}
		// finally, add the thread to publish the actual blog entry to the publish sequence
		publishSequence.add(publish);
		// start the first thread in the sequence.
		publishProgress.setMessage("Starting publishing...");
		publishSequence.get(0).start();
		// From here on, each thread executes, reporting it's status through the mHandler message handler.
		// When each thread is finished, it calls the runnable "mThreadFinished". This then spawns the next thread
		// in the publish sequence.
	}
	
	private void showFinalPublishedStatus() {
		publishProgress.dismiss();
		if(publishStatus == 5) {
			AlertUtil.showAlert(this,"Publish status","Publish Ok!");
			// activity should finish after ok.
		} else {
			AlertUtil.showAlert(this,"Publish status","Publish failed! (Code "+publishStatus+")");
		}
	}
	
	private int findListIndexOfConfig(LinkedList<BlogConfigBean> items, int lookfor) {
		for(int i = 0; i < items.size();i++) {
			BlogConfigBean inspect = items.get(i);
			if(inspect != null) {
				if(inspect.getId() == lookfor) {
					return i;
				}
			} else {
				Log.e(TAG,"Received a null configBean from DB!");
				return -1;
			}
		}
		return -1;
	}
	
}